package com.trainmodel.eu.z21helper;

import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.NetworkInterface;
import java.util.Collections;
import java.util.Enumeration;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class CenterSide implements Runnable {

	private String centerHost = "192.168.0.111";
	private String localCenterHost = "0.0.0.0";

	private int centerPort = 21105;
	private int localCenterPort = 21105;

	private EmulatorSide emulatorSide = null;

	private InetAddress centerAddress;
	private DatagramSocket centerSocket;

	private Z21Helper z21helper = null;

	private long txCounter = 0L;
	private long rxCounter = 0L;

	public CenterSide(String centerHost) {
		if (centerHost != null)
			this.centerHost = centerHost;

		// Detect default ip here
		localCenterHost = detectLocalCenterHost();
		System.out.println("Will bind Z21-side socket to " + localCenterHost);
	}

	private String detectLocalCenterHostWindows() throws UnableToDetectIPException {
		String defaultHost = null;
		Process process;
		try {
			process = Runtime.getRuntime().exec("route print");
			process.waitFor();
			BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
			String line = reader.readLine();
			Pattern iface = Pattern.compile("^.*0\\.0\\.0\\.0\\s+0\\.0\\.0\\.0\\s+\\S+\\s+(\\S+).+$");
			while (line != null) {
				Matcher m = iface.matcher(line);
				if (m.matches()) {
					defaultHost = m.group(1);
					break;
				}
				line = reader.readLine();
			}
			System.out.println("Detected windows default IP: " + defaultHost);

		} catch (Exception e) {
			throw new UnableToDetectIPException();
		}
		if (!isValidIPv4(defaultHost))
			throw new UnableToDetectIPException();
		return defaultHost;
	}

	private String detectLocalCenterHostOSX() throws UnableToDetectIPException {
		String interfaceName = null;
		String defaultHost = null;
		try {
			Process process = Runtime.getRuntime().exec("netstat -rn");
			process.waitFor();
			BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
			String line = reader.readLine();
			Pattern iface = Pattern.compile("^.*default.+\\s+(\\S+).*$");
			while (line != null) {
				Matcher m = iface.matcher(line);
				if (m.matches()) {
					interfaceName = m.group(1);
					break;
				}
				line = reader.readLine();
			}
			System.out.println("Default interface: " + interfaceName);
			Enumeration<NetworkInterface> nets = NetworkInterface.getNetworkInterfaces();
			for (NetworkInterface netint : Collections.list(nets))
				if (netint.getName().equals(interfaceName)) {
					for (InetAddress ip : Collections.list(netint.getInetAddresses())) {
						System.out.println("IP of this interface: " + ip.getHostAddress());
						if (!ip.isLoopbackAddress() && !ip.isLinkLocalAddress() && !ip.isAnyLocalAddress() && isValidIPv4(ip.getHostAddress())) {
							defaultHost = ip.getHostAddress();
							break;
						}
					}
					break;
				}
			System.out.println("Detected default IP: " + defaultHost);
		} catch (Exception e) {
			throw new UnableToDetectIPException();
		}
		return defaultHost;
	}

	private boolean isValidIPv4(String ip) {
		Pattern p = Pattern.compile("^(?:[0-9]{1,3}\\.){3}[0-9]{1,3}$");
		Matcher m = p.matcher(ip);
		if (m.matches())
			return true;
		return false;
	}

	private String detectLocalCenterHost() {
		String os = System.getProperty("os.name");
		try {
			if (os.startsWith("Windows")) {
				return detectLocalCenterHostWindows();
			} else {
				return detectLocalCenterHostOSX();
			}
		} catch (UnableToDetectIPException e) {
			return detectSimilarIP(this.centerHost);
		}
	}

	private String detectSimilarIP(String targetHost) {
		InetAddress similarIP;
		try {
			InetAddress targetAddress = InetAddress.getByName(targetHost);
			similarIP = InetAddress.getByName("0.0.0.0");
			Enumeration<NetworkInterface> nets = NetworkInterface.getNetworkInterfaces();
			for (NetworkInterface netint : Collections.list(nets)) {
				for (InetAddress ip : Collections.list(netint.getInetAddresses())) {
					if (ip.isAnyLocalAddress() || ip.isLinkLocalAddress() || ip.isLoopbackAddress() || ip.isMulticastAddress()
							|| !isValidIPv4(ip.getHostAddress()))
						continue;
					System.out.println("Testing IP: " + ip.getHostAddress());
					similarIP = mostSimilarIPv4Address(targetAddress, similarIP, ip);
				}
			}
		} catch (Exception e) {
			return "0.0.0.0";
		}
		return similarIP.getHostAddress();
	}

	private InetAddress mostSimilarIPv4Address(InetAddress similarTo, InetAddress pretender1, InetAddress pretender2) {
		if (similarTo == null || pretender2 == null)
			return pretender1;
		if (pretender1 == null)
			return pretender2;
		if (!isValidIPv4(similarTo.getHostAddress()) || !isValidIPv4(pretender1.getHostAddress()) || !isValidIPv4(pretender2.getHostAddress()))
			return pretender1;
		byte[] s = similarTo.getAddress();
		byte[] p1 = pretender1.getAddress();
		byte[] p2 = pretender2.getAddress();
		for (int i = 0; i < s.length; i++) {
			if (s[i] == p1[i] && s[i] != p2[i])
				return pretender1;
			if (s[i] != p1[i] && s[i] == p2[i])
				return pretender2;
		}
		return pretender1;

	}

	public void sendToCenter(DatagramPacket packet) {
		try {
			if (centerAddress != null && centerSocket != null) { // test if initialized
				packet.setAddress(centerAddress);
				packet.setPort(centerPort);
				centerSocket.send(packet);
				addTxCounter(packet.getLength());
			}
		} catch (Exception e) {
			if (z21helper != null)
				z21helper.errorAndQuit("Unable to sent packet to Z21 center.\n\n" + e.getMessage());
		}
	}

	@Override
	public void run() {
		try {
			centerAddress = InetAddress.getByName(centerHost);
			centerSocket = new DatagramSocket(localCenterPort, InetAddress.getByName(localCenterHost));
			while (true) {
				DatagramPacket p = new DatagramPacket(new byte[2500], 2500);
				centerSocket.receive(p);
				emulatorSide.sendToEmulator(p);
				addRxCounter(p.getLength());
			}

		} catch (Exception e) {
			if (z21helper != null)
				z21helper.errorAndQuit("Unable to open socket to communicate with Z21 center.\nIs one Z21 Helper already running? Close it and start again.\n\n"
						+ e.getMessage());
		}
	}

	public String getCenterHost() {
		return centerHost;
	}

	public void setCenterHost(String centerHost) {
		this.centerHost = centerHost;
	}

	public String getLocalCenterHost() {
		return localCenterHost;
	}

	public void setLocalCenterHost(String localCenterHost) {
		this.localCenterHost = localCenterHost;
	}

	public int getCenterPort() {
		return centerPort;
	}

	public void setCenterPort(int centerPort) {
		this.centerPort = centerPort;
	}

	public int getLocalCenterPort() {
		return localCenterPort;
	}

	public void setLocalCenterPort(int localCenterPort) {
		this.localCenterPort = localCenterPort;
	}

	public EmulatorSide getEmulatorSide() {
		return emulatorSide;
	}

	public void setEmulatorSide(EmulatorSide emulatorSide) {
		this.emulatorSide = emulatorSide;
	}

	public synchronized long getTxCounter() {
		return txCounter;
	}

	private synchronized void addTxCounter(long bytes) {
		this.txCounter += bytes;
	}

	public synchronized long getRxCounter() {
		return rxCounter;
	}

	private synchronized void addRxCounter(long bytes) {
		this.rxCounter += bytes;
	}

	public synchronized void resetCounters() {
		this.txCounter = 0L;
		this.rxCounter = 0L;
	}

	public Z21Helper getZ21helper() {
		return z21helper;
	}

	public void setZ21helper(Z21Helper z21helper) {
		this.z21helper = z21helper;
	}

}
